﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System.IO;
using Microsoft.StyleCop;
using System.Reflection;
using SPStyleCop.Test.Util;

namespace SPStyleCop.Test
{
    /// <summary>
    /// BaseUnitTest, the baseline for all unit test.
    /// This class has been created using ideas from http://contribstylecop.codeplex.com 
    /// Some simplification may have been made from the original version.
    /// Add Generics to the expected violation entry.
    /// </summary>
    [TestClass]
    public abstract class BaseUnitTest<TExpectation> : IDisposable
    {
        #region Fields

        private readonly string _projectRootPath;
        private readonly IList<Expectation<TExpectation>> _expectations;
        private ConsoleRunner _consoleRunner;
        private string _codeFile;
        private AnalysisResults _analysisResults;

        private bool _isDisposed;
        private string _projectSettingsPath;

        #endregion

        #region Constructors

        /// <summary>
        /// Initialize BaseunitTest class.
        /// </summary>
        public BaseUnitTest()
        {
            this._projectRootPath=Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
            this._expectations = new List<Expectation<TExpectation>>();
        }

        #endregion

        #region Methods

        /// <summary>
        /// Configure test environment variables. 
        /// </summary>
        [TestInitialize]
        public void Setup()
        {
            this._expectations.Clear();
            this._codeFile = null;
            this._analysisResults = null;
            this.TestSetup();
        }

        /// <summary>
        /// Additional TestSetup() by the child class.
        /// </summary>
        protected virtual void TestSetup()
        {
        }

        /// <summary>
        /// SetTargetFile for testing
        /// </summary>
        /// <param name="relativeFilePathFromProjectRoot">Relative path of the target file</param>
        protected void SetTargetFile(string relativeFilePathFromProjectRoot)
        {
            this._codeFile = Path.Combine(this._projectRootPath, relativeFilePathFromProjectRoot);
        }

        /// <summary>
        /// SetBaseProjectSettingFile
        /// </summary>
        /// <param name="relativeFilePathFromProjectRoot">Relative path of the Settings.StyleCop</param>
        protected void SetBaseProjectSettingFile(string relativeFilePathFromProjectRoot)
        {
            this._projectSettingsPath = Path.Combine(this._projectRootPath, relativeFilePathFromProjectRoot);
        }

        /// <summary>
        /// Analyzing the source code
        /// </summary>
        protected void Analyze()
        {
            if (this._codeFile == null) throw new InvalidOperationException("TargetFile must be set before analysis");

            string settingsPath = this.GetSettingsFilePath();

            this._consoleRunner = new ConsoleRunner(settingsPath, null);

            CodeProject codeProject = ProjectUtility.CreateOneFileProject(this._codeFile, this._consoleRunner.Environment);

            this._analysisResults = this._consoleRunner.Analyze(codeProject);
        }

        /// <summary>
        /// Add Expectation of type TExpectation.
        /// </summary>
        /// <param name="violatedRule">TExpectation</param>
        /// <param name="lineNumbers">Line numbers of the error</param>
        protected void AddExpectation(TExpectation violatedRule, params int[] lineNumbers)
        {
            foreach (int lineNumber in lineNumbers)
            {
                this._expectations.Add(new Expectation<TExpectation> { ViolatedRule = violatedRule, LineNumber = lineNumber });
            }
        }

        /// <summary>
        /// Validate Expected errors
        /// </summary>
        protected void ValidateExpectations()
        {
            var actualViolations = new List<Violation>(this._analysisResults.Violations);
            IList<Expectation<TExpectation>> unmatchedExpectations = new List<Expectation<TExpectation>>();
            
            foreach (Expectation<TExpectation> expectation in this._expectations)
            {
                var matchingViolations  = actualViolations.RemoveAll( v => v.Rule.Name == expectation.ViolatedRule.ToString() &&
                                                   v.Line == expectation.LineNumber);
                
                if ( matchingViolations <= 0)
                {
                    unmatchedExpectations.Add(expectation);
                } 
            }

            BaseUnitTest<TExpectation>.ReportUnmatchedViolations(unmatchedExpectations, actualViolations);
        }


        private string GetSettingsFilePath()
        {
            return this._projectSettingsPath;
        }

        private static void ReportUnmatchedViolations(ICollection<Expectation<TExpectation>> unmatchedExpectations,
                                                      ICollection<Violation> unmatchedViolations)
        {
            StringBuilder message = new StringBuilder();

            if (unmatchedExpectations.Count > 0)
            {
                message.AppendLine("The following expectations where not found in the code file");
                foreach (Expectation<TExpectation> expectation in unmatchedExpectations)
                {
                    message.AppendLine(expectation.ViolatedRule + " at line " + expectation.LineNumber);
                }
            }

            if (unmatchedViolations.Count > 0)
            {
                message.AppendLine("The following violations where not expected in the code file");
                
                foreach (Violation violation in unmatchedViolations)
                {                    
                    message.AppendLine(violation.Rule.Name + " at line " + violation.Line);
                }
            }

            if (message.Length > 0) Assert.Fail(message.ToString());
        }

        private void Dispose(bool isDisposing)
        {
            if (!this._isDisposed)
            {
                if (isDisposing)
                {
                    if (this._consoleRunner != null)
                    {
                        this._consoleRunner.Dispose();
                    }
                }

                this._isDisposed = true;
            }
        }

        #endregion

        #region IDispose

        public void Dispose()
        {
            this.Dispose(true);
            GC.SuppressFinalize(this);
        }

        #endregion

        #region Nested Types

        private sealed class Expectation<TExpectation>
        {
            public TExpectation ViolatedRule { get; set; }

            public int LineNumber { get; set; }
        }

        #endregion
    }
}
